package neuralNetworks.evolution;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import neuralNetworks.Connection;
import neuralNetworks.Layer;
import neuralNetworks.NeuralNetworkFactory;
import neuralNetworks.Neuron;
import neuralNetworks.tool.ISaveable;
import neuralNetworks.tool.MultiNetLoader;

public class Evolver<T extends EvolutionNet> implements ISaveable {

	List<Pattern> patterns;

	List<T> generation;

	double mutationFactor;

	public int steps = 0;

	public int age = 0;

	int saveAll = 0;

	String path;

	MultiNetLoader<T> netload;

	NeuralNetworkFactory<T> netFact;

	public ExecutorService fpool = Executors.newFixedThreadPool(8);

	public Evolver(MultiNetLoader<T> netload, Class<T> netClass, double mutationFactor, String path, int saveAll) {
		this.netload = netload;
		this.generation = netload.nets;
		this.mutationFactor = mutationFactor;
		this.path = path.substring(0, path.lastIndexOf("/"));
		this.age = Integer.parseInt(path.substring(path.lastIndexOf("/") + 1));
		this.saveAll = saveAll;
		this.netFact = new NeuralNetworkFactory<>(netClass, Neuron.class);
	}

	public Evolver(List<T> generation, Class<T> netClass, List<Pattern> patterns, double mutationFactor) {
		this.patterns = patterns;
		this.generation = generation;
		this.mutationFactor = mutationFactor;
		this.netFact = new NeuralNetworkFactory<>(netClass, Neuron.class);
	}

	public Evolver(List<T> generation, Class<T> netClass, double mutationFactor) {
		this.generation = generation;
		this.mutationFactor = mutationFactor;
		this.netFact = new NeuralNetworkFactory<>(netClass, Neuron.class);
	}

	public void evolve() throws InstantiationException, IllegalAccessException, InterruptedException,
			ExecutionException {
		// get fitness and sort by
		List<Future<Double>> futfits = new ArrayList<>(generation.size());
		for (final EvolutionNet nn : generation) {
			futfits.add(fpool.submit(new Callable<Double>() {
				@Override
				public Double call() throws Exception {
					return getFitness(nn);
				}
			}));
		}
		for (int i = 0; i < generation.size(); i++) {
			generation.get(i).fitness = futfits.get(i).get();
		}
		Collections.sort(generation);
		// get fittest to mutate
		int keep = (int) (generation.size() * (1. - mutationFactor));
		int replace = generation.size() - keep;
		List<Future<T>> futmuts = new ArrayList<>(replace);
		for (int i = 0, t = 0; i < replace; i++, t++) {
			if (t >= keep)
				t = 0;
			final T mutnet = generation.get(t);
			futmuts.add(fpool.submit(new Callable<T>() {
				@Override
				public T call() throws Exception {
					return mutate(mutnet);
				}
			}));
		}
		for (int i = 0; i < replace; i++) {
			generation.set(keep + i, futmuts.get(i).get());
		}
		steps++;
		if (path != null && steps % saveAll == 0) {
			save();
		}

	}

	public void save() {
		String dir = path + "/" + (steps + age);
		netload.saveAll(path + "/" + (steps + age));
		System.out.println("Saved: " + dir);
	}

	public T mutate(T nn) throws InstantiationException, IllegalAccessException {
		T mutated = netFact.copy(nn);
		for (Layer<Neuron> layer : mutated.layer) {
			for (Neuron neuron : layer.neurons) {
				for (Connection<? extends Neuron> conn : neuron.sendConns) {
					double rndfactor = (Math.random() * 2.) - 1.;
					conn.weight += rndfactor + Math.pow(rndfactor, 3) * conn.weight;
				}
				double rndfactor = (Math.random() * 2.) - 1.;
				neuron.bias += rndfactor + Math.pow(rndfactor, 3) * neuron.bias;
			}
		}
		return mutated;
	}

	public double getFitness(EvolutionNet nn) {
		double errorSum = 0.;
		for (Pattern pattern : patterns) {
			double[] ip = pattern.input;
			double dop = pattern.output;
			double op = nn.run(ip);
			double err = Math.abs(dop - op);
			errorSum += err;
		}
		double fitness = 1. - (errorSum / (double) patterns.size());
		// add random value
		// fitness += Math.random() / 1000.;
		return fitness;
	}

	public List<Pattern> getPatterns() {
		return patterns;
	}

	public void setPatterns(List<Pattern> patterns) {
		this.patterns = patterns;
	}

}
